# Merise python parser and models generators.
#
# @autor: Patrice Ferlet <metal3d@gmail.com>
# @version 0.0.2
# @licence Gnu/GPL v3

# $Id: parser.py 40 2010-07-28 07:54:32Z Metal3d $
# $HeadURL: http://merisier.googlecode.com/svn/tags/0.0.2-alpha2/src/merise/parser.py $

"""
Parser is used to read merise definition file
and fetch data definitions, relations and types

merise files defined datas without relation fields at first, then you can
find relations. Look at this example:

[customer]
+id
first_name
last_name
birthdate date not null

[products]
+id
name
price float(10.2)
description

[bill]
+id
bill_date date not null
payed int(1) default 0

#define relations
# table1 relation_to_table2 description backward_relation table2 (some attributes)
#where relation_to_table2 and backward_relation can be:
#0,n - 1,n - 1,1 - 0,1
#attributes set some additionnal fields...

#look example

#customer can pay 0 or X bill
#bill must have only one customer:

customer 0,n payement 1,1 bill

#a bill is composed by products (with quantity)
#bill have at least one product
#a product can be in no bill...

bill 1,n bill_composition 0,n products (quantity int default 1)

That's all !
"""
import os
import re

class Parser:
    """
    Class that parse merise file
    """
    def __init__(self, options):
        """
        Initialize Parser with options (given from command line, see merise.py file)
        """
        self.defs = {}
        self.links = []
        self.arrows = []
        self.keys = {}
        self.types = {}
        self.options = options
        
    def parse(self):
        """
        Parse file to find entities and relations
        """
        current = ''
        f = open(self.options.filename)

        for line in f:
            #remove comments, new lines etc...
            line=line.replace(",\n","")
            line=line.replace("\n","")
            line = re.sub(r'^#.*','',line)
            line=line.lower()
            
            #check if we are working on entity or relations
            d = re.findall(r'\[(.*)\]', line)
            d2 = re.findall(r'([^ ]*)\s+([0-9n,]*)\s+([^ ]*)\s+([0-9n,]*)\s+([^ \(]*)\s*(\(.*\))*$', line)
            
            #entity
            if len(d) > 0:
                self.defs[d[0]] = []
                current = d[0]
            else :
                #fields
                if len(d2) == 0:
                    parts = line.split(' ')
                    _attr = parts.pop(0)
                    _type = " ".join(parts)
                    
                    #it's a key
                    if re.search('\+', _attr):
                        self.keys[current] = _attr.replace('+','')
                        try: 
                            len(self.types[current])
                        except:
                            self.types[current] = []
                        #_attr = _attr.replace('+','')
                        if len(_type.strip())==0:
                            _type = "auto"
                        #types is defined as tuple: (field, type, primary key, relation table, is null)
                        self.types[current].append( (_attr.replace('+','') , _type, False, None,False) )
                    else:
                        self.types[current].append( (_attr , _type, False, None, True) )
                    self.defs[current].append(_attr)
                #relations
                else:
                    self.links.append(d2[0])

    def getId(self,table):
        """
        Check if key completion must be id_table or table_id (see options)
        """
        if self.options.idpos == 'right':
            return table+'_'+self.keys[table]
        return self.keys[table]+'_'+table

    def findCardinals (self):
        """
        Find cardinalities table x,x relation x,x table2 (attributes if possible)
        Example:
            Bill 1,n compose 0,n Products (quantity int default 1)
            You must read:
            Bill is composed by 1 or n Products with quantity
            Products can be in 0 or n Bill
        """
        for rel in self.links:
            (table1, num1, link, num2 , table2, attr) = rel
            
            #card is X,1 X,n
            if re.search(',1', num1) and  (re.search(',n', num2) or re.search(',1', num2)):
                #find if table1 reference could be null,
                null = False
                if re.search('0,', num1):
                    null = True
                    
                self.defs[table1].append(self.getId(table2))
                for (f,t,i,i2,nullity) in self.types[table2]:
                    if self.keys[table2]==f:
                        _type = t
                        
                self.types[table1].append((self.getId(table2), _type,True, table2,null))
                self.arrows.append (( (table2, self.getId(table2)) , (table1,  table1+"_"+self.getId(table2))))
                                
            if re.search(',n', num1) and  re.search(',1', num2):
                #find if table2 reference could be null,
                null = False
                if re.search('0,', num2):
                    null = True
                                
                self.defs[table2].append(self.getId(table1))
                for (f,t,i,i2,nullity) in self.types[table1]:
                    if self.keys[table1]==f:
                        _type = t
                self.types[table2].append((self.getId(table1), _type,True, table1,null))
                #self.arrows.append ((self.keys[table2]+"_"+table2 , table2+"_"+"id_"+table1))
                self.arrows.append (( (table1, self.getId(table1)) , (table2,  table2+"_"+self.getId(table1))))
                
            #card X,n X,n
            if re.search(',n', num1) and  re.search(',n', num2):
                #create a cardinal table
                cardinal = link
                try:
                    if self.defs[cardinal] is None:
                        self.defs[cardinal] = []
                        self.types[cardinal] = []
                except:
                    self.defs[cardinal] = []
                    self.types[cardinal] = []
            
                if  self.getId(table1) not in self.defs[cardinal]:
                    null = False
                    if re.search('0,', num1):
                        null = True
                    #TODO for now we force not null
                    null = False
                    self.defs[cardinal].append(self.getId(table1))
                    for (f,t,i,i2,nullity) in self.types[table1]:
                        if self.keys[table1]==f:
                            _type = t
                            
                    self.types[cardinal].append((self.getId(table1), _type, True, table1,null))
                    self.arrows.append( ((cardinal, cardinal+"_"+self.getId(table1) )  ,  (table1, self.getId(table1) )) )
                if  self.getId(table2) not in self.defs[cardinal]:
                    null = False
                    if re.search('0,', num2):
                        null = True
                    #TODO for now we force not null
                    null = False
                    self.defs[cardinal].append(self.getId(table2))
                    for (f,t,i,i2,nullity) in self.types[table2]:
                        if f and self.keys[table2]==f:
                            _type = t
                    self.types[cardinal].append((self.getId(table2), _type, True, table2,null))
                    self.arrows.append( ((cardinal, cardinal+"_"+self.getId(table2) )  ,  (table2, self.getId(table2) )) )
                    
                if len(attr.strip()):
                    attr = re.findall(r'\((.*)\)', attr)
                    attr = attr[0].split(',')
                    for a in attr:
                        parts = a.strip().split(' ');
                        _attr = parts.pop(0)
                        self.defs[cardinal].append(_attr)
                        if len(parts):
                            _type = " ".join(parts)
                        else:
                            _type = "varchar(255)"
                        self.types[cardinal].append((_attr, _type, False, None,None))
				
				
